// Copyright 2012 The Glue BSF Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview This file contains the base call for the glue.View object.
 */

goog.provide('glue.View');

goog.require('glue.iController');
goog.require('glue.iView');
goog.require('glue.EventType');

goog.require('goog.array');

goog.require('goog.async.Deferred');

goog.require('goog.dom');
goog.require('goog.dom.classes');

goog.require('goog.events.EventHandler');
goog.require('goog.events.EventType');
goog.require('goog.events.KeyHandler');
goog.require('goog.events.KeyHandler.EventType');

goog.require('goog.structs.Set');

goog.require('goog.string');

goog.require('goog.style');

goog.require('goog.Timer');

goog.require('goog.ui.Component');
goog.require('goog.ui.Css3ButtonRenderer');
goog.require('goog.ui.decorate');
goog.require('goog.ui.Zippy');
goog.require('goog.ui.ZippyEvent');


/**
 * Base view implementation.
 * This class should be used as the base for any View you add to the system.
 * Overriding the decorateInteral and enterDocument usually is sufficient to produce some stellar results.
 * 
 * @param {goog.dom.DomHelper=} opt_domHelper DOM helper to use.
 * @extends {goog.ui.Component}
 * @constructor
 * @export
 * @public
 * @implements {glue.iView}
 */
glue.View = function(opt_domHelper){
	goog.base(this,opt_domHelper);
	
	/**
	 * Set used to store the urls used by the deferred processes.
	 * Anything in this set will be used for assessing conditional revalidate re-renders.
	 * @type goog.structs.Set
	 * @protected
	 */
	this.urls_ = new goog.structs.Set();
	
	/**
	* Event handler for this object.
	* @type goog.events.EventHandler
	* @protected 
	*/
	this.eh_ = new goog.events.EventHandler(this);
	
	/**
	* Keyboard handler for this object. 
	* This object is created once the component's DOM element is known.
	* 
	* @type goog.events.KeyHandler|Null
	* @protected
	*/
	this.kh_ = null;
	
	this.outStanding_ = 0;
	this.processing_ = false;
};
goog.inherits(glue.View, goog.ui.Component);

/**
 * Name of the View
 * 
 * @type {string}
 * @export
 * @public
 */
glue.View.prototype.ViewName = '';

/**
 * This is the method that glue.Component uses to get the view name.  
 * This is used as a work around to the used with Closure or not problem.
 * 
 * @export
 * @public
 * @final
 * @return {String} the view name
 */
glue.View.prototype.getViewName = function(){
	var name = this.ViewName;
	if(goog.string.isEmptySafe(name)){
		name = this['ViewName'];		
	}
	return name;
	
}

/**
 * Add the Controller to the View
 * 
 * @param {kpcompass.ve.iController} controller The Controller object as set by the View Controller 
 */
glue.View.prototype.setController = function(controller){
	/**
	 * Access to the Controller
	 * @type glue.iController
	 * @private
	 */
	this.controller_ = controller;
	
	this.eh_.listen(controller,glue.EventType.REVALIDATE,goog.bind(function(event){		
		if(goog.array.some(this.urls_.getValues(),function(url){
			return goog.array.contains(event.data,url)
		})){			
			this.handleRevalidate();
		}
		
	},this))
};

/**
 * Automatically re-renders the control with the new data;
 */
glue.View.prototype.handleRevalidate = function(){
	
	if(this.outStanding_ > 0){		
		goog.Timer.callOnce(goog.bind(this.handleRevalidate,this),100)
	}else if(!this.processing_){
		this.processing_ = true
		goog.Timer.callOnce(function(){
			this.forEachChild(function(child){
				child.dispose()
			},this)
			
			var element = this.getContentElement()	
			goog.dom.removeChildren(element)
				
			this.decorateInternal(element)
			this.enterDocument();
			this.processing_ = false;
		},0,this);
			
	}
	
}

/**
 * Get the Controller
 * 
 * @return {glue.iController} The Controller object as set by the View Controller
 * @export
 * @public
 * @final
 */
glue.View.prototype.getController = function(){
	return this.controller_;
};

/**
 * Creates an initial DOM representation for the component.
 */
glue.View.prototype.createDom = function() {
	this.decorateInternal(goog.dom.createDom('div'));
};

/**
 * Decorates an existing HTML DIV element as a SampleComponent.
 * 
 * @param {Element} element The DIV element to decorate. 
 * @public
 */
glue.View.prototype.decorateInternal = function(element) {
	goog.base(this,'decorateInternal',element);
	this.setElementInternal(element);
};


/**
 * @export
 * @public
 * @override 
 */
glue.View.prototype.disposeInternal = function() {
	goog.base(this,'disposeInternal');	
	this.eh_.dispose();
	if (this.kh_) {
		this.kh_.dispose();
	}
};

/**
 * Called when component's element is known to be in the document.
 * 
 * @export
 * @public
 */
glue.View.prototype.enterDocument = function() {
	goog.base(this,'enterDocument');	
	this.addListeners();
};

/**
 * Location to organize event listeners into.
 * 
 * @export
 * @public
 */
glue.View.prototype.addListeners = function(){};

/**
 * Called when component's element is known to have been removed from the document.
 * 
 * @export
 * @public
 */
glue.View.prototype.exitDocument = function() {
	goog.base(this,'exitDocument');	
	this.removeListeners();	
};

/**
 * Event to remove external listeners.  
 * Dispose will take care of all the normal listeners attached to the event handler but
 * there might be a class where you would want to remove an external listener.
 * 
 * @export
 * @public
 */
glue.View.prototype.removeListeners = function(){};

/**
 * Pass in either an element or element id and 
 * it will return an element that represents the inner content block.
 * The title is the first apparent header.  
 * We sometimes use hidden headers to force different collapsed headers.  
 *
 * For Example:
 * 
 * this.deferredProcess('/api/something/',this.makeCollapsible('someId'),namespace.someComponent);
 * 
 * @param {string|Element} parent the element id or element to make collapsible.
 * @param {Function?} opt_cb Call back function that takes 1 parameter the function to collapse the collapsible programmatically.
 * @export
 * @public
 * @final
 * @return {Element} inner content component element
 */
glue.View.prototype.makeCollapsible = function(parent,opt_cb){
	if(goog.isString(parent)){
		parent = goog.dom.getElement(parent);
	}
	
	var holder = goog.dom.createDom('div',{'class':'kp-zippy-header'}); 
	var header = goog.dom.createDom('div',{'class':'hidden'})
	var button = goog.dom.createDom('div',{'class':'kp-text-button goog-css3-button','style':'float:right;'},'-');
	goog.dom.append(holder,button);
	goog.dom.append(holder,header);
	var b = goog.ui.decorate(button);	
	
	var inner = goog.dom.createDom('div');
	goog.dom.append(parent,holder);
	
	goog.dom.append(parent,inner);
	var zippy = new goog.ui.Zippy(button,inner,true);
	
	var first = true;
	
	function zippyToggle(event) {
		if(first){
			goog.array.some(['3','4','5','6'],goog.bind(function(l){
				var headers = goog.dom.getElementsByTagNameAndClass('h'+l,undefined,inner);
				var result = false;
				if(goog.isDefAndNotNull(headers) && headers.length > 0){
					var item = headers[0];
					var s = goog.array.reduce(item.children,function(r,v){
						r = r || '';
						return r+goog.dom.getTextContent(v);
					});
					
					if(s === " ")s = "";
										
					var name = goog.dom.getTextContent(item).replace(s,'')
					goog.dom.append(header,goog.dom.createDom('h'+l,{'class':'glue-zippy-header'},name));
						
					result = !result;
				}		
				return result;
			},this));
			
					
			first = false;
		}
		goog.dom.classes.toggle(header,'hidden')
		
		if(goog.isDefAndNotNull(event) && event.expanded){     	
      		button.innerHTML = '-'
      	} else{      	
      		button.innerHTML = '+'
      	};	      
    }
	
	this.eh_.listen(zippy, goog.ui.Zippy.Events.TOGGLE, zippyToggle);
	
	if(goog.isFunction(opt_cb)){
		opt_cb(function(opt_expand){
			opt_expand = opt_expand||false;
			if(opt_expand){
				zippy.expand()
			}else{
				zippy.collapse()	
			}
			
			
		})
		
	}
	return inner;
}

/**
 * Function to apply some base styling to views.
 * 
 * @param {Element} element Element to apply decoration to.
 * @protected
 * @export
 */
glue.View.prototype.style = function(element){
	
	var elements = goog.dom.getElementsByClass('goog-css3-button',element);
	goog.array.map(elements,goog.bind(function(e){
		goog.style.setInlineBlock(e);
		var button = goog.ui.decorate(e);		
		button.setDispatchTransitionEvents(goog.ui.Component.State.ALL,true);
		if(e.children.length === 1 && e.children[0].tagName === 'A'){
			this.eh_.listen(button,goog.ui.Component.EventType.ACTION,function(event){
				var href = e.children[0].getAttribute('href').replace('#','');				
				window.location.hash = goog.string.trim(href);
			})			
		}
	},this));
};

/**
 * Internal function tieing all the deferred and direct processing 
 * that used a function instead of a component into the view.
 * 
 * @param {*} parent
 * @param {string|array|*} html
 * @param {Function} actions
 * 
 * @private
 */
glue.View.prototype.processElement_ = function(parent,html,actions){
	if(goog.isArray(html)){
		html = html.join('');
	}
	
	/** @type {*} */
	var frags;
	if(goog.isString(html)){
		frags = goog.dom.htmlToDocumentFragment(html);	
	}else{
		frags = html;
	}
	
	if(goog.dom.getChildren(parent).length>0){
		goog.dom.replaceNode(frags,parent);	
	}else{
		goog.dom.appendChild(parent,frags);
	}	
	
	if(goog.isFunction(actions)){
		actions(frags);
	}
	this.style(frags);
};

/**
 * Takes arbitrary data and applies the data to formatter in conjuction with parent.
 * 
 * @param {*} data 
 * @param {String|Element} parent
 * @param {Function|glue.View} formatter
 * @param {Bool?} opt_arguementArray defaults to false
 * 
 * @export
 * @public
 * @final
 */
glue.View.prototype.directProcess = function(data,parent,formatter,opt_arguementArray){
	if(goog.isString(parent)){
		parent = goog.dom.getElement(parent);
	}

    var returns;    
    if(opt_arguementArray){
        returns = formatter.apply(this||window,data);
    }else{
        returns = formatter(data);
    }
    
	if(returns === undefined){		
		var component = new formatter(this.params_,data);
		
		if(component instanceof glue.View){
			component.setController(this.getController());
		}
		
		var child = this.getChild(component.getId())
		if(!goog.isDefAndNotNull(child)){
			this.addChild(component);	
		}		
		component.render(parent);
		this.style(component.getContentElement())		
	}else{			
		this.processElement_(parent,returns.html||'',returns.actions||function(instance){});	
	}
};

/**
 * add a url to the watch list
 * 
 * @param {String} url
 *  
 * @export
 * @public
 * @final
 */
glue.View.prototype.addUrl = function(url){
	this.urls_.add(url);
}

/**
 * Retrieves data from an url and passes the result to the directProcess.
 * 
 * @param {String} url 
 * @param {String|Element} parent
 * @param {Function|glue.View} formatter
 * @param {Bool?} opt_arguementArray defaults to false
 * 
 * @export
 * @public
 * @final
 */
glue.View.prototype.deferredProcess = function(url,parent,formatter,opt_arguementArray){
	this.urls_.add(url)
	this.outStanding_ +=1;
	this.getController().get(url,goog.bind(function(data){
		this.directProcess(data,parent,formatter,opt_arguementArray);
		this.outStanding_ -=1;
	},this));
};

/**
 * Takes an array of urls or data objects and retrieves that data.
 * Once all the data is retrieved it passes it as an array to the directProcess
 * with the argumentArray parameter set to true.
 * 
 * @param {Array} urls An array of urls or data objects.
 *     Yes this breaks the idea arrays should be homogeneous data 
 *     but arrays are really objects in JS so this does not break them.
 *     If you know a better solution please submit a patch.
 * @param {String|Element} parent
 * @param {Function|glue.View} formatter
 *  
 * @export
 * @public
 * @final
 */
glue.View.prototype.deferredComposite = function(urls,parent,formatter){
	var compositeData = {};
	var deferred;
	var lock = 0;
	this.outStanding_ +=1;
	var fireDirect = goog.bind(function(){		
		if(lock === 0){
            var argues = [];
            goog.array.forEach(urls,function(url){
            	if(goog.isString(url)){
            		argues.push(compositeData[url]);	
            	}else{
            		argues.push(compositeData[goog.getUid(url)]);
            	}
            });
            this.outStanding_ -=1;
			this.directProcess(argues,parent,formatter,true);
		}
	},this);
	
	goog.array.forEach(urls,goog.bind(function(url){		
		if(goog.isString(url)){
			this.urls_.add(url);	
			var result = this.getController().get(url,function(data){
				compositeData[url] = data;
			});
			if(!result.hasFired){			
				lock++;			
				var test = new goog.async.Deferred();
				test.addCallback(function(data){
					compositeData[url] = data;
					lock--;				
					fireDirect();
				});
				result.deferred.chainDeferred(test);
			}	
		}else{
			this.urls_.add(goog.getUid(url));
			compositeData[goog.getUid(url)] = url;
		}	
		
	},this));
	fireDirect();
};

/**
 * This function was a hold over from the original methodology that did not have a centralized ajax cache.
 *   
 * @deprecated use a series of deferredProcess(url,parent,formatter,argumentArray)
 * 
 * @param {String} url
 * @param {Array} formatterTokens
 * 
 * @export
 * @public
 * @final
 */
glue.View.prototype.deferredList = function(url,formatterTokens){
	goog.array.forEach(formatterTokens,goog.bind(function(formatterToken){
		this.deferredProcess(url,formatterToken.parent,formatterToken.formatter);
	},this));
};

